﻿using System;
using System.Collections.Generic;
using System.Text;
using Vortex.Drawing;
using Vortex.Input;
using System.Windows.Forms;
using Vortex.Debugging;

namespace Vortex.SceneFramework.Gui {

	/// <summary>
	/// Single line text input widget. It supports styles but text is always aligned by left side
	/// </summary>
	public class TextInput : Label {
		private char[] CHARS_TO_IGNORE = new char[] { '\n', '\r', '\t' }; 
		protected int _maxTextLength = 260;
		protected int _cursorPosition = 0;
		protected float _inputTime = 0;
		protected float _cursorBlinkTime = 0.6f;

		protected string _undoText = null;
		protected int _undoCursorPosition = 0;

		/// <summary>
		/// Initializes a new instance of the <see cref="TextInput"/> class.
		/// </summary>
		public TextInput() {
			//input should definitely be focusable
			IsFocusable = true;
		}

		/// <summary>
		/// Gets or sets the maximal length of the text. Default value is 260.
		/// </summary>
		/// <value>The maximal length of the text.</value>
		public int MaxTextLength {
			get { return _maxTextLength; }
			set {
				if (value < 1) throw new ArgumentException("Max text length should be at least 1.", "MaxTextLength");
				if (_maxTextLength != value) {
					_maxTextLength = value;
					if (Text.Length > _maxTextLength) {
						Text = Text.Substring(0, _maxTextLength);
					}
				}
			}
		}

		/// <summary>
		/// Gets or sets the input text.
		/// </summary>
		/// <value>The input text.</value>
		public override string Text {
			get { return base.Text; }
			set {
				string text = value;
				if (null != text) {
					text = CleanupText(text);
					//check trim text length
					if (text.Length > MaxTextLength) {
						text = text.Substring(0, MaxTextLength);
					}
				}
				if (base.Text != text) {
					//preserve undo...
					PreserveUndo();
					//set text when everything is processed
					base.Text = text;
					//check bound cursor
					MoveCursorTo(CursorPosition);
				}
			}
		}

		/// <summary>
		/// Gets or sets the text edditing cursor position.
		/// </summary>
		/// <value>The text edditing cursor position.</value>
		public int CursorPosition {
			get { return _cursorPosition; }
			set {
				MoveCursorTo(value);
			}
		}

		/// <summary>
		/// Gets or sets the cursor blink time in seconds (valid range: 0.05 - 2.0).
		/// </summary>
		/// <value>The cursor blink time in seconds.</value>
		public float CursorBlinkTime {
			get { return _cursorBlinkTime; }
			set { _cursorBlinkTime = Math.Max(Math.Min(value, 2.0f), 0.05f); }
		}

		#region Text Processing

		/// <summary>
		/// Processes the char input event.
		/// </summary>
		/// <param name="eventInfo">The keyboard event info.</param>
		protected internal override void ProcessChar(KeyboardEventInfo eventInfo) {
			//only letter chars are allowed
			if (eventInfo.Char >= ' ') {
				InsertChar(eventInfo.Char);
			}
			base.ProcessChar(eventInfo);
		}

		/// <summary>
		/// Processes the key down input event.
		/// </summary>
		/// <param name="eventInfo">The keyboard event info.</param>
		protected internal override void ProcessKeyDown(KeyboardEventInfo eventInfo) {
			switch (eventInfo.Key) {
				case VirtualKey.Left:
					MoveCursor(-1);
					break;
				case VirtualKey.Right:
					MoveCursor(1);
					break;
				case VirtualKey.Home:
					MoveCursorTo(0);
					break;
				case VirtualKey.End:
					MoveCursorTo(Text.Length);
					break;
				case VirtualKey.Delete:
					DoDelete();
					break;
				case VirtualKey.Backspace:
					DoBackspace();
					break;
				case VirtualKey.V:
					if (eventInfo.IsControlDown) {
						PasteFromClipboard();
					}
					break;
				case VirtualKey.Z:
					if (eventInfo.IsControlDown) {
						Undo();
					}
					break;
			}
			base.ProcessKeyDown(eventInfo);
		}

		/// <summary>
		/// Undoes this text input.
		/// </summary>
		private void Undo() {
			if (null != _undoText) {
				string undoText = _undoText;
				int undoCursorPosition = _undoCursorPosition;
				_undoText = Text;
				_undoCursorPosition = CursorPosition;
				Text = undoText;
				MoveCursorTo(undoCursorPosition);
			}
		}

		/// <summary>
		/// Preserves the undo information.
		/// </summary>
		private void PreserveUndo() {
			//save undo text...
			_undoText = Text;
			_undoCursorPosition = CursorPosition;
		}

		/// <summary>
		/// Pastes text from clipboard into specified position.
		/// </summary>
		private void PasteFromClipboard() {
			try {
				IDataObject dataObj = Clipboard.GetDataObject();

				if (dataObj.GetDataPresent(DataFormats.UnicodeText)) {
					InsertText(dataObj.GetData(DataFormats.UnicodeText).ToString());
				} else {
					if (dataObj.GetDataPresent(DataFormats.Text)) {
						InsertText(dataObj.GetData(DataFormats.Text).ToString());
					}
				}

			} catch (Exception e) {
				Log.Error(e, "Unable paste text from clipboard. Probably your main function doesn't have [STAThreadAttribute]");
			}
		}

		/// <summary>
		/// Makes the cleanup of special characters for text.
		/// </summary>
		/// <param name="text">The text to cleanup.</param>
		/// <returns>Text without special characters</returns>
		private string CleanupText(string text) {
			if (text.IndexOfAny(CHARS_TO_IGNORE) >= 0) {
				text = text.Replace(@"\n", "").Replace(@"\r", "").Replace(@"\t", "");
			}
			return text;
		}

		/// <summary>
		/// Inserts the text at the current cursor position.
		/// </summary>
		/// <param name="text">The text to insert.</param>
		public void InsertText(string text) {
			if (null == text) throw new ArgumentNullException("text");
			text = CleanupText(text);
			int insertedTextLength = text.Length;
			Text = Text.Substring(0, _cursorPosition) + text + Text.Substring(_cursorPosition);
			MoveCursor(insertedTextLength);
		}

		/// <summary>
		/// Inserts the char at the current cursor position.
		/// </summary>
		/// <param name="charCode">The char code to insert.</param>
		public void InsertChar(char charCode) {
			if (charCode >= 32) {
				Text = Text.Substring(0, _cursorPosition) + charCode + Text.Substring(_cursorPosition);
				MoveCursor(1);
			}
		}

		/// <summary>
		/// Does the backspace action at current caret location.
		/// </summary>
		public void DoBackspace() {
			if (_cursorPosition > 0) {
				Text = Text.Substring(0, _cursorPosition - 1) + Text.Substring(_cursorPosition);
				if (_cursorPosition < Text.Length) {
					MoveCursor(-1);
				}
			}
		}

		/// <summary>
		/// Does the delete action at current caret location.
		/// </summary>
		public void DoDelete() {
			if (_cursorPosition < Text.Length) {
				Text = Text.Substring(0, _cursorPosition) + Text.Substring(_cursorPosition + 1);
			}
		}

		/// <summary>
		/// Moves the cursor caret on a specified offset.
		/// </summary>
		/// <param name="relMove">The rel move.</param>
		public void MoveCursor(int caretOffset) {
			MoveCursorTo(_cursorPosition + caretOffset);
		}

		/// <summary>
		/// Moves the cursor caret to specified location.
		/// </summary>
		/// <param name="cursorPosition">The caret position.</param>
		public void MoveCursorTo(int caretPosition) {
			_cursorPosition = Math.Max(Math.Min(caretPosition, Text.Length), 0);
			_inputTime = 0;
		}

		#endregion

		#region Mouse Down Processing

		/// <summary>
		/// Processes the mouse down event text input.
		/// </summary>
		/// <param name="eventInfo">The mouse event info.</param>
		protected internal override void ProcessMouseDown(MouseEventInfo eventInfo) {
			//what should we do here... on left mouse down we should set cursor on visible element
			if (eventInfo.Button == MouseButton.Left) {
				SetCursorClosestToPoint(eventInfo.Location);
			}

			base.ProcessMouseDown(eventInfo);
		}

		/// <summary>
		/// Sets the cursor closest to the specified point.
		/// </summary>
		/// <param name="point">The point to use in calculations.</param>
		private void SetCursorClosestToPoint(Vector2 point) {
			Rect textRegion = GetTextRegion();

			string text = Text;
			SpriteFont font = Style.Get<SpriteFont>(Styles.TextFont);

			float textPositionX = GetTextDrawingPoint(ref textRegion, font).X;

			int bestCursorPosition = -1;
			float closestDistance = float.MaxValue;
			
			int firstCharIndex = IsFocused ? FindFirstCharIndex(font, textRegion.Width) : 0;
			
			//interate through all of characters
			for (int n = firstCharIndex; n < text.Length; ++n) {
				float distance = Math.Abs(point.X - textPositionX);
				if (distance < closestDistance) {
					closestDistance = distance;
					bestCursorPosition = n;
				}

				if (textPositionX > textRegion.Right) {
					break;
				}

				textPositionX += GetCharWidth(font, text[n]);
			}

			//set cursor at point...
			MoveCursorTo(bestCursorPosition);
		}

		#endregion

		#region Text Control Update

		/// <summary>
		/// Update input element for this frame time change
		/// </summary>
		/// <param name="timeDelta">The time delta in seconds.</param>
		public override void Update(float timeDelta) {
			if (IsFocused) {
				_inputTime = ((_inputTime + timeDelta) % (_cursorBlinkTime * 2));
			}
		}

		#endregion

		#region Drawing

		/// <summary>
		/// TextInput drawing method. It draws background behind and editable text foreground
		/// </summary>
		/// <param name="canvas">The canvas to use for text input drawing.</param>
		protected override void Draw(Canvas2D canvas) {
			//draw background first
			DrawBackground(canvas, BoundingBox);

			//draw editable text
			DrawEditableText(canvas);
		}

		/// <summary>
		/// Draws the editable text with cursor at.
		/// </summary>
		/// <param name="canvas">The canvas for drawing.</param>
		private void DrawEditableText(Canvas2D canvas) {
			Rect textRegion = GetTextRegion();

			string text = Text;
			SpriteFont font = Style.Get<SpriteFont>(Styles.TextFont);

			Vector2 drawingPoint = GetTextDrawingPoint(ref textRegion, font);
			Vector2 cursorPoint = Vector2.Zero;

			int firstCharIndex = IsFocused ? FindFirstCharIndex(font, textRegion.Width) : 0;
			//interate through all of characters
			for (int n = firstCharIndex; n < text.Length; ++n) {
				if (n == _cursorPosition) {
					cursorPoint = drawingPoint;
				}

				char charCode = text[n];
				SpriteCharInfo charInfo = font[charCode] ?? font[Canvas2D.MissingCharCode];
				if (null != charInfo) {
					float nextCharPosX = drawingPoint.X + GetCharWidth(font, text[n]);
					if (nextCharPosX < textRegion.Right) {
						canvas.DrawSprite(drawingPoint, charInfo.Glyph, ColorU.Black);
					}
					drawingPoint.X = nextCharPosX;
				}
			}
			//check if cursor in the end of string?...
			if (_cursorPosition >= text.Length) {
				cursorPoint = drawingPoint;
			}

			//check should we draw cursor)
			if (IsFocused && _inputTime < _cursorBlinkTime && cursorPoint.X < textRegion.Right) {
				//draw cursor over as line for now
				canvas.DrawFilledRect(cursorPoint.X - 1, cursorPoint.Y, 2, font.LineHeight, ColorU.Black);
			}
		}

		/// <summary>
		/// Finds the first index of the char for the current cursor poisition, font and available width.
		/// </summary>
		/// <param name="font">The font.</param>
		/// <param name="availableWidth">Width of the available.</param>
		/// <returns></returns>
		private int FindFirstCharIndex(SpriteFont font, float availableWidth) {
			//check very special cases...
			if (CursorPosition == 0) {
				return 0;
			}

			string text = Text;
			float maxRightPartWidth = availableWidth / 3;
			float consumedRigthPartWidth = 0;
			//checking right part width
			if (CursorPosition < text.Length) {
				for (int n = CursorPosition; n < text.Length; ++n) {
					float nextWidth = consumedRigthPartWidth + GetCharWidth(font, text[n]);
					if (nextWidth > maxRightPartWidth) {
						break;
					}
					consumedRigthPartWidth = nextWidth;
				}
			}
			//calculate what is left for left part...
			float maxLeftPartWidth = availableWidth - (consumedRigthPartWidth + 1);
			float consumedLeftPartWidth = 0;
			for (int n = CursorPosition - 1; n >= 0; --n) {
				float nextWidth = consumedLeftPartWidth + GetCharWidth(font, text[n]);
				if (nextWidth > maxLeftPartWidth) {
					return n + 1;
				}
				consumedLeftPartWidth = nextWidth;
			}
			//thats is good, return first char...
			return 0;
		}

		/// <summary>
		/// Gets the width of the char.
		/// </summary>
		/// <param name="font">The font to use.</param>
		/// <param name="charCode">The char code.</param>
		/// <returns></returns>
		private float GetCharWidth(SpriteFont font, char charCode) {
			SpriteCharInfo charInfo = font[charCode] ?? font[Canvas2D.MissingCharCode];
			if (null != charInfo) {
				return charInfo.Size.X;
			} else {
				return 0;
			}
		}

		/// <summary>
		/// Gets the start text drawing point.
		/// </summary>
		/// <param name="textRegion">The text region with padding subtracted.</param>
		/// <param name="font">The font to use.</param>
		/// <returns></returns>
		private Vector2 GetTextDrawingPoint(ref Rect textRegion, SpriteFont font) {
			Vector2 offsetTop = textRegion.LeftTop;
			TextLayout layout = Style.Get<TextLayout>(Styles.TextLayout);
			if (textRegion.Height > font.LineHeight) {
				if ((layout & TextLayout.Bottom) == TextLayout.Bottom) {
					offsetTop.Y += (float)Math.Floor((textRegion.Height - font.LineHeight) * (((layout & TextLayout.Top) == TextLayout.Top) ? 0.5f : 1));
				}				
			}
			return offsetTop;
		}

		/// <summary>
		/// Gets the text drawing region.
		/// </summary>
		/// <returns></returns>
		private Rect GetTextRegion() {
			return BoundingBox.Deflate(Style.Get<Rect>(Styles.TextPadding));
		}


		#endregion

	}
}
